Qutebrowser 如何通过程序选择页面元素

我热衷于对 qutebrowser 进行二次开发。在本文中,探讨一个核心操作,如何选择页面中的元素。

从 Tab 说起

qutebrowser 中,每个 Tab 的类型为 AbstractTab,其内部有一个核心成员:

elements: AbstractElements

elements 表示所有的页面元素。对页面元素的访问问题,就是对 elements 的访问问题。

AbstractTab 中的 elements

AbstractTab 是抽象类,之包含了 elements 的声明,不包含赋值。赋值操作在子类中完成。AbstractTab 有两个子类 WebEngineTab 和 WebKitTab。这里以 WebEngineTab 为例,创建实例:

self.elements = WebEngineElements(tab=self)

WebEngineElements 的 find 系列方法

在 WebEngineElements 中,定义有一系列 find 方法:

find_css

这里以 find_css 为例。

Python 侧实现

首先先看基类 AbstractElements 中的声明:

_MultiCallback = Callable[[Sequence['webelem.AbstractWebElement']], None]

def find_css(self, selector: str,
                callback: _MultiCallback,
                error_cb: _ErrorCallback, *,
                only_visible: bool = False) -> None:
    """Find all HTML elements matching a given selector async.

    If there's an error, the callback is called with a webelem.Error
    instance.

    Args:
        callback: The callback to be called when the search finished.
        error_cb: The callback to be called when an error occurred.
        selector: The CSS selector to search for.
        only_visible: Only show elements which are visible on screen.
    """
    raise NotImplementedError

其中:通过 callback 回调返回页面元素。并且类型是 AbstractWebElement 序列

再看 QWebEngineTab 的实现:

def find_css(self, selector, callback, error_cb, *,
                only_visible=False):
    js_code = javascript.assemble('webelem', 'find_css', selector,
                                    only_visible)
    js_cb = functools.partial(self._js_cb_multiple, callback, error_cb)
    self._tab.run_js_async(js_code, js_cb)

可见,页面元素搜索,实际上是执行了一段 JavaScript。

JavaScript 侧实现

而这段 JavaScript,是 qutebrowser 提前预埋进去的。位于 webelem.js

funcs.find_css = (selector, only_visible) => {
    let elems;

    try {
        elems = document.querySelectorAll(selector);
    } catch (ex) {
        return {"success": false, "error": ex.toString()};
    }

    const subelem_frames = window.frames;
    const out = [];

    for (let i = 0; i < elems.length; ++i) {
        if (!only_visible || is_visible(elems[i])) {
            out.push(serialize_elem(elems[i]));
        }
    }

    // Recurse into frames and add them
    // ...

    return {"success": true, "result": out};
};

预埋时机

位于 _WebEngineScripts 类中,该类掌管了向 WebEngineTab 中注入的各类元素(JavaScript、CSS、greasemonkey、quirks)等。对 webelem.js 的注入位于其 init 方法中:

def init(self):
    """Initialize global qutebrowser JavaScript."""
    js_code = javascript.wrap_global(
        'scripts',
        resources.read_file('javascript/scroll.js'),
        resources.read_file('javascript/webelem.js'),
        resources.read_file('javascript/caret.js'),
    )
    # FIXME:qtwebengine what about subframes=True?
    self._inject_js('js', js_code, subframes=True)
    self._init_stylesheet()

    self._greasemonkey.scripts_reloaded.connect(
        self._inject_all_greasemonkey_scripts)
    self._inject_all_greasemonkey_scripts()
    self._inject_site_specific_quirks()

回调中 AbstractWebElement 的是怎么来的

在上面代码中,为什么在 JavaScript 侧执行完 find_css,回到 Python 的 callback 回调中,就变成了 AbstractWebElement 类型?

首先,在 find_css 的 JavaScript 代码中,注意到有一个 serialize_elem 方法,他会在 JavaScript 侧获取该元素的各种属性。

再回到 Python 侧的以下实现:

def find_css(self, selector, callback, error_cb, *,
                only_visible=False):
    js_code = javascript.assemble('webelem', 'find_css', selector,
                                    only_visible)
    js_cb = functools.partial(self._js_cb_multiple, callback, error_cb)
    self._tab.run_js_async(js_code, js_cb)

_js_cb_multiple 的实现如下:

def _js_cb_multiple(self, callback, error_cb, js_elems):
    """Handle found elements coming from JS and call the real callback.

    Args:
        callback: The callback to call with the found elements.
        error_cb: The callback to call in case of an error.
        js_elems: The elements serialized from javascript.
    """
    if js_elems is None:
        error_cb(webelem.Error("Unknown error while getting "
                                "elements"))
        return
    elif not js_elems['success']:
        error_cb(webelem.Error(js_elems['error']))
        return

    elems = []
    for js_elem in js_elems['result']:
        elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
        elems.append(elem)
    callback(elems)

从中可以看出:_js_cb_multiple 将 JavaScript 侧封装的元素数据,保存在 WebEngineElement 中。而 WebEngineElement 则是在 Python 侧操作的对象。

思考题:XPath 支持

qutebrowser 默认并不支持 xpath。通过上述如法炮制,不难扩展出 XPath 支持。

这里记录一个实现思路:

这样,便实现了通过 xpath,返回一组 WebEngineElement 序列的功能。


本文作者:Maeiee

本文链接:Qutebrowser 如何通过程序选择页面元素

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!